package main

import (
	"bufio"
	"encoding/base64"
	"flag"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"strings"
	"text/template"
	"time"
)

var (
	port              = flag.String("port", "8085", "")
	output            = flag.String("out", "gfwlist.rules", "save the output to the file, default stdout")
	autoproxylist_url = flag.String("url", "https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt", "the autoproxy gfwlist url, defaullt: https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt")
	show_version      = flag.Bool("version", false, "show version and exit")
)

const (
	Help = `Usage: gfwlist2pac [OPTION]...
	-port		http listen port : 8085
    -proxy      special proxy address, ex: 127.0.0.1:8080
    -proxy_type special proxy type, ex: HTTP,SOCKS5, HTTPS, etc
    -out        save the output to the file, default stdout
    -url        the autoproxy gfwlist url, defaullt: https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt
    -help       show help and exit
    -version    show version and exit
`

	Version     = "gfwlist2pac 0.0.1"
	gfwlist_url = "https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt"
)

type args struct {
	/* proxy address */
	Proxy string

	/* gfwlist domains name */
	Domains map[string]int

	/* custom domains name */
	Custom map[string]int
}

func main() {
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "%s", Help)
		os.Exit(1)
	}
	flag.Parse()

	if *show_version {
		fmt.Printf("%s\n", Version)
		os.Exit(0)
	}

	go func() {
		resolve()
		timer := time.NewTicker(time.Hour * 24)
		for {
			select {
			case <-timer.C:
				resolve()
			}
		}
	}()

	fmt.Println("Listen Port :" + *port)
	http.HandleFunc("/proxyauto.pac", ProxyAutoServer)
	err := http.ListenAndServe(":"+*port, nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

func ProxyAutoServer(w http.ResponseWriter, r *http.Request) {

	_, err := os.Stat(*output)

	if err != nil && os.IsNotExist(err) {
		resolve()
	}

	w.Header().Add("Content-Type", "application/x-ns-proxy-autoconfig")

	r.ParseForm()

	proxy_host := r.FormValue("host")
	proxy_scheme := r.FormValue("scheme")

	if proxy_host == "" {
		proxy_host = "127.0.0.1:8580"
	}
	if proxy_scheme == "" {
		proxy_scheme = "http"
	}
	proxy_scheme = strings.ToUpper(proxy_scheme)
	aa := args{
		Proxy: fmt.Sprintf("%s %s", proxy_scheme, proxy_host),
	}

	/* read template file */
	t, err := template.ParseFiles(*output)
	if err != nil {
		log.Fatal(err)
	}

	err = t.Execute(w, aa)
	if err != nil {
		log.Fatal(err)
	}

}

func resolve() {
	uri := gfwlist_url
	if *autoproxylist_url != "" {
		uri = *autoproxylist_url
	}

	res, err := http.Get(uri)
	if err != nil {
		log.Fatal(err)
	}

	decoder := base64.NewDecoder(base64.StdEncoding, res.Body)

	var domains map[string]int = map[string]int{}

	reader := bufio.NewReader(decoder)
	for {
		line, _, err := reader.ReadLine()
		if err != nil {
			if err != io.EOF {
				log.Print(err)
			}
			break
		}

		s := parse(string(line))
		if s != "" {
			if _, ok := domains[s]; !ok {
				domains[s] = 1
			}
		}
	}

	res.Body.Close()

	/* template arguments */
	aa := args{
		"{{ .Proxy }}", //fmt.Sprintf("%s %s", *proxy_type, *proxy),
		domains,
		read_custom_list("user-rule.rules"),
	}

	/* read template file */
	t, err := template.ParseFiles("pac.tmpl")
	if err != nil {
		log.Fatal(err)
	}

	out := os.Stdout
	if *output != "" {
		/* custom output */
		out, err = os.Create(*output)
		if err != nil {
			log.Fatal(err)
		}
		defer out.Close()
	}

	/* output */
	err = t.Execute(out, aa)
	if err != nil {
		log.Fatal(err)
	}
}

/*
parse autoproxy gfwlist line,
return domain name
*/
func parse(line string) string {

	/* remove space */
	line = strings.Trim(line, " ")

	if line == "" {
		return ""
	}

	/* ignore ip address */
	if net.ParseIP(line) != nil {
		return ""
	}

	/* ignore pattern */
	if strings.Index(line, ".") == -1 {
		return ""
	}

	/* ignore comment, whitelist, regex */
	if line[0] == '[' ||
		line[0] == '!' ||
		line[0] == '/' ||
		line[0] == '@' {
		return ""
	}

	return getHostName(line)
}

func getHostName(line string) string {
	c := line[0]
	ss := line

	/* replace '*' */
	if strings.Index(ss, "/") == -1 {
		if strings.Index(ss, "*") != -1 && ss[:2] != "||" {
			ss = strings.Replace(ss, "*", "/", -1)
		}
	}

	switch c {
	case '.':
		ss = fmt.Sprintf("http://%s", ss[1:])
	case '|':
		switch ss[1] {
		case '|':
			ss = fmt.Sprintf("http://%s", ss[2:])
		default:
			ss = ss[1:]
		}
	default:
		if strings.HasPrefix(ss, "http") {
			ss = ss
		} else {
			ss = fmt.Sprintf("http://%s", ss)
		}
	}

	/* process */
	u, err := url.Parse(ss)
	if err != nil {
		log.Printf("%s: %s\n", line, err)
		return ""
	}
	host := u.Host
	if n := strings.Index(host, "*"); n != -1 {
		for i := n; i < len(host); i++ {
			if host[i] == '.' {
				host = host[i:]
				break
			}
		}
	}
	return strings.TrimLeft(host, ".")
}

/*
read the custom domain list,
one domain per line
*/
func read_custom_list(fn string) map[string]int {
	if fn == "" {
		return map[string]int{}
	}

	fp, err := os.Open(fn)
	if err != nil {
		log.Println(err)
		return map[string]int{}
	}

	defer fp.Close()

	domains := map[string]int{}

	reader := bufio.NewReader(fp)

	for {
		line, _, err := reader.ReadLine()
		if err != nil {
			if err != io.EOF {
				log.Printf("%s\n", err)
			}
			break
		}

		ll := string(line)

		if ll != "" {
			if _, ok := domains[ll]; !ok {
				domains[ll] = 1
			}
		}
	}

	return domains
}
